﻿using CodeGenerator.CSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Modifier = CodeGenerator.CSharp.Modifiers;

namespace CodeAngel.Domain
{
    /// <summary>
    /// Generates the aggregate file for an AggregateDef.  If this file already exists, it will
    /// be overwritten.
    /// </summary>
    public static class AggregateFileGenerator
    {
        #region CreateAggregateFileAsync

        /// <summary>
        /// Creates and writes to disk the delete aggregate file for the specified EntityDef.
        /// If this file already exists, it will be overwritten.
        /// </summary>
        public static Task CreateAggregateFileAsync(this AggregateDef thisAD)
        {
            return Task.Factory.StartNew(() =>
            {
                AggregateFileGenerator.WriteToDisk(thisAD);
            });
        }

        #endregion

        #region WriteToDisk

        /// <summary>
        /// Creates and writes to disk an aggregate file for the identified AggregateDef. 
        /// If this file already exists, it will be overwritten.
        /// </summary>
        private static void WriteToDisk(AggregateDef thisAD)
        {
            var ThisFB = new FileBuilder();
            ThisFB.ProjectName = thisAD.EntityDef.Domain.ProjectName;
            ThisFB.Directory = thisAD.Directory;
            ThisFB.FileName = thisAD.FileName;
            ThisFB.Namespace = thisAD.Namespace;

            // Add the using namespaces for this file.
            ThisFB.UsingNamespaces.Add("System");
            ThisFB.UsingNamespaces.Add("System.Linq");
            ThisFB.UsingNamespaces.Add("System.Threading.Tasks");
            ThisFB.UsingNamespaces.Add(thisAD.EntityDef.Domain.FrameworkNamespace);

            // Add the DTO class.
            ThisFB.CodeUnits.Add(new AggregateClassBuilder(thisAD));

            // Write this file to disk.
            ThisFB.WriteToDisk(OverwriteExistingFile.Yes);
        }

        #endregion

        #region AggregateClassBuilder

        /// <summary>
        /// Builds the aggregate class.
        /// </summary>
        private class AggregateClassBuilder : ClassBuilder
        {
            #region Constructors

            public AggregateClassBuilder(AggregateDef thisAD)
                : base(thisAD.ClassName, Modifier.PublicPartial)
            {
                this.BaseClass = "Aggregate";
                this.Documentation.Summary = thisAD.Description;

                //if (thisAD.ClassName == "UserLogInAggregateDef") this.AddLoadAsyncForUserLogInAggregateDef();
                if (thisAD.ClassName == "UserLogInAggregate") this.AddLoadAsyncForUserLogInAggregate();

                this.AddRootProperty(thisAD);
                this.AddCreateNewProcedure(thisAD);
                this.AddLoadAsyncProcedure(thisAD);
                this.AddIsFilterTypeValidProcedure(thisAD);
                this.AddBuildObjectGraphProcedure(thisAD);
                this.AddSynchronizeDtosProcedure(thisAD);
                this.AddUserHasPermissionToSaveProcedure(thisAD);

                //ConditionalCompilation FullClrRegion = this.AddFullClrConditionalCompilation;
                //this.AddGetDtoHost(FullClrRegion);
                //this.AddGetUpdateableDtos(FullClrRegion);

            }

            #endregion

            #region AddRootProperty

            /// <summary>
            /// Adds the Root property.
            /// </summary>
            private void AddRootProperty(AggregateDef thisAD)
            {
                Region ThisRegion = this.AddRegion("Root");
                var ThisProperty = ThisRegion.AddProperty("Root", Modifier.PublicNew, thisAD.GetRootDataTypeAsString());
                ThisProperty.GetBody.Add("return base.Root as {0};", thisAD.GetRootDataTypeAsString());
            }

            #endregion

            #region AddCreateNewProcedure

            private void AddCreateNewProcedure(AggregateDef thisAD)
            {
                // CreateNew is not applicable toCollection based Roots.
                if (thisAD.RootIsCollection) return;

                var ThisRegion = this.AddRegion("CreateNew");

                var CreateNewProcedure = ThisRegion.AddProcedure("CreateNew", Modifier.Public);
                CreateNewProcedure.Documentation.Summary = "Creates the root of this aggreate as a new entity.";
                CreateNewProcedure.Documentation.Remarks = "The currently logged in user will be used for security purposes.";
                CreateNewProcedure.Body.Add("this.CreateNew(User.LoggedInUser);");

                var CreateNewWithUserProcedure = ThisRegion.AddProcedure("CreateNew", Modifier.Public);
                CreateNewWithUserProcedure.Documentation.Summary = "Creates the root of this aggreate as a new entity/collection.";
                CreateNewWithUserProcedure.Parameters.Add("user", "User", "Identifies the user which will be used for secruity purposes.");
                CreateNewWithUserProcedure.Body.Add("var ThisRoot = new {0}(this);", thisAD.EntityDef.ClassNames.ClassName());
                CreateNewWithUserProcedure.Body.Add("this.User = user;");
                CreateNewWithUserProcedure.Body.Add("ThisRoot.IsNew = true;");
                CreateNewWithUserProcedure.Body.Add("ThisRoot.InitializeAllProperties();");
                CreateNewWithUserProcedure.Body.Add("ThisRoot.SetDefaultPropertyValues();");
                CreateNewWithUserProcedure.Body.Add("ThisRoot.BuildObjectGraphCompleted();");
                CreateNewWithUserProcedure.Body.Add("this.DtoHost = new DtoHost();");
                CreateNewWithUserProcedure.Body.Add("base.Root = ThisRoot;");
            }


            #endregion

            #region AddLoadAsyncProcedure

            /// <summary>
            /// Adds the LoadAsync procedure used to get the data of this aggregate.
            /// </summary>
            private void AddLoadAsyncProcedure(AggregateDef thisAD)
            {
                Region ThisRegion = this.AddRegion("LoadAsync");

                if (thisAD.RootIsCollection)
                {
                    var LoadAsyncProcedure = ThisRegion.AddProcedure("LoadAsync", Modifier.Public, "Task");
                    LoadAsyncProcedure.Documentation.Summary = "Instructs this aggregate to asynchronously load its object graph based on the identified filter.  The currently logged in user (see User.LoggedInUser) will be used for security related purposes.";
                    LoadAsyncProcedure.Parameters.Add("filter", thisAD.EntityDef.ClassNames.Filter());
                    LoadAsyncProcedure.Body.Add("return this.LoadAsync(filter, User.LoggedInUser);");

                    var LoadAllAsyncProcedure = ThisRegion.AddProcedure("LoadAllAsync", Modifier.Public, "Task");
                    LoadAllAsyncProcedure.Documentation.Summary = "Instructs this aggregate to asynchronously load its object graph will all entities of this type.  The currently logged in user (see User.LoggedInUser) will be used for security related purposes.";
                    LoadAllAsyncProcedure.Body.Add("return this.LoadAsync(new " + thisAD.EntityDef.ClassNames.Filter() + "(), User.LoggedInUser);");
                }
                else
                {
                    var LoadAsyncProcedure = ThisRegion.AddProcedure("LoadAsync", Modifier.Public, "Task");
                    LoadAsyncProcedure.Documentation.Summary = "Instructs this aggregate to asynchronously load its object graph based on the identified id.  The currently logged in user (see User.LoggedInUser) will be used for security related purposes.";
                    LoadAsyncProcedure.Parameters.Add("id", "Guid");
                    LoadAsyncProcedure.Body.Add("return this.LoadAsync(new " + thisAD.EntityDef.ClassNames.Filter() + "(id), User.LoggedInUser);");

                }

            }


            #endregion

            #region AddIsFilterTypeValidProcedure

            private void AddIsFilterTypeValidProcedure(AggregateDef thisAD)
            {
                Region ThisRegion = this.AddRegion("IsFilterTypeValid");
                var IsFilterTypeValidProcedure = ThisRegion.AddProcedure("IsFilterTypeValid", Modifier.ProtectedOverride, "bool");
                IsFilterTypeValidProcedure.Documentation.Summary = "Determines if the passed in filter is valid for this aggregate.";
                IsFilterTypeValidProcedure.Parameters.Add("filter", "IFilter");
                IsFilterTypeValidProcedure.Body.Add("return (filter is {0});", thisAD.EntityDef.ClassNames.Filter());
            }

            #endregion

            #region AddBuildObjectGraphProcedure

            /// <summary>
            /// Add the BuildObjectGraph procedure to this aggregate.
            /// </summary>
            private void AddBuildObjectGraphProcedure(AggregateDef thisAD)
            {
                var ThisRegion = this.AddRegion("BuildObjectGraph");

                var CustomBuildObjectGraphProcedure = ThisRegion.AddProcedure("CustomBuildObjectGraph", Modifier.Partial);
                CustomBuildObjectGraphProcedure.Documentation.Summary = "Provides a way for the developer to custom build the object graph for this aggregate.  Be sure to set the _BuildObjectGraphIsHandled field to true to avoid having the auto-generated BuildObjectGraph code executed.";

                var BuildObjectGraphIsHandledField = ThisRegion.AddField("_BuildObjectGraphIsHandled", Modifier.Private, "bool");
                BuildObjectGraphIsHandledField.Documentation.Summary = "Informs the auto-generated BuildObjectGraph method that building the object graph has already been handled and it does not need to do anything.";
                BuildObjectGraphIsHandledField.DefaultValue = "false";

                var ThisProcedure = ThisRegion.AddProcedure("BuildObjectGraph", Modifier.ProtectedOverride);
                ThisProcedure.Documentation.Summary = "";

                ThisProcedure.Body.AddComment("Offers the developer a way to build the object graph by hand.");
                ThisProcedure.Body.Add("this.CustomBuildObjectGraph();");
                ThisProcedure.Body.Add("if (this._BuildObjectGraphIsHandled) return;");
                ThisProcedure.Body.AddBlankLine();

                ThisProcedure.Body.Add("try");
                ThisProcedure.Body.Add("{");

                ThisProcedure.Body.Add("var ThisRoot = new {0}(this);", thisAD.GetRootDataTypeAsString());
                if (thisAD.RootIsCollection)
                {
                    string EntityVariableName = "This" + thisAD.EntityDef.Name;
                    ThisProcedure.Body.Add("foreach (var {0} in this.DtoHost.Root{1})", thisAD.EntityDef.VariableNames.CreateDtosFromDataReader(), thisAD.EntityDef.Name.ToPlural());
                    ThisProcedure.Body.Add("{");
                    ThisProcedure.Body.Add("    var {0} = new {1}(this);", EntityVariableName, thisAD.EntityDef.Name);
                    ThisProcedure.Body.Add("    ThisRoot.Add(This{0});", thisAD.EntityDef.Name);
                    this.AddDataElements(ThisProcedure.Body, thisAD.DataElement, EntityVariableName, thisAD.EntityDef.VariableNames.CreateDtosFromDataReader());
                    ThisProcedure.Body.Add("}");
                }
                else
                {
                    ThisProcedure.Body.Add("var {0} = this.DtoHost.Root{1}[0];", thisAD.EntityDef.VariableNames.CreateDtosFromDataReader(), thisAD.EntityDef.Name.ToPlural());
                    this.AddDataElements(ThisProcedure.Body, thisAD.DataElement, "ThisRoot", thisAD.EntityDef.VariableNames.CreateDtosFromDataReader());
                }

                ThisProcedure.Body.Add("base.Root = ThisRoot;");

                ThisProcedure.Body.Add("}");
                ThisProcedure.Body.Add("catch (Exception ex)");
                ThisProcedure.Body.Add("{");
                ThisProcedure.Body.Add("throw new Exception(\"There was an unexpected problem building the object graph.  See inner exception for more info.\", ex);");
                ThisProcedure.Body.Add("}");


            }

            /// <summary>
            /// Adds the Value type, Entity type and Collection type elements.
            /// </summary>
            private void AddDataElements(Body body, DataElement parentDE, string parentVariable, string dtoName)
            {
                AddValueTypeDataElements(body, parentDE, parentVariable, dtoName);
                AddEntityDataElements(body, parentDE, parentVariable, dtoName);
                AddCollectionDataElements(body, parentDE, parentVariable, dtoName);
            }

            /// <summary>
            /// 
            /// </summary>
            private void AddCollectionDataElements(Body body, DataElement parentDE, string parentVariable, string dtoName)
            {
                foreach (DataElement ThisDE in parentDE.DataElements.CollectionTypes)
                {
                    var ThisPD = ThisDE.PropertyDef;
                    int Suffix = this.GetNextVariableSuffix();
                    string EntityVariableName = ThisPD.Dt.ToBaseName() + Suffix;
                    string DtoVariableName = ThisPD.Dt.ToBaseName() + "Dto" + Suffix;

                    body.AddBlankLine();
                    body.Add("{0}.Initialize{1}();", parentVariable, ThisPD.Names.BackingProperty());
                    body.Add("foreach (var {0} in this.DtoHost.{1}.FindAll(ThisDto => ThisDto.{2}.Equals({3}.ID)))", DtoVariableName, ThisPD.DataTypeEntityDef.Name.ToPlural(), ThisPD.ForeignKeyPropertyDef.Names.Field(), dtoName);
                    body.Add("{");
                    body.Add("var {0} = new {1}(this);", EntityVariableName, ThisPD.DataTypeEntityDef.ClassNames.ClassName());
                    body.Add("{0}.{1}.Add({2});", parentVariable, ThisPD.Name, EntityVariableName);
                    if ((ThisPD.ForeignKeyPropertyDef.IsSomething()))
                    {
                        body.Add("{0}.Initialize{1}({2});", EntityVariableName, ThisPD.ForeignKeyPropertyDef.Names.BackingProperty(), parentVariable);
                    }
                    this.AddDataElements(body, ThisDE, EntityVariableName, DtoVariableName);
                    body.Add("}");

                }

            }

            /// <summary>
            /// 
            /// </summary>
            private void AddEntityDataElements(Body body, DataElement parentDE, string parentVariable, string dtoName)
            {
                foreach (DataElement ThisDE in parentDE.DataElements.EntityTypes)
                {
                    PropertyDef ThisPD = ThisDE.PropertyDef;
                    int Suffix = this.GetNextVariableSuffix();
                    string EntityVariableName = ThisPD.Name + Suffix;
                    string DtoVariableName = ThisPD.Name + "Dto" + Suffix;

                    body.AddBlankLine();
                    body.Add("var {0} = this.DtoHost.{1}.Find<{2}>({3}.{4});", DtoVariableName, ThisPD.DataTypeEntityDef.Name.ToPlural(), ThisPD.DataTypeEntityDef.ClassNames.Dto(), dtoName, ThisPD.Names.Field());
                    body.Add("if ({0} != null)", DtoVariableName);
                    body.Add("{");

                    body.Add("    var {0} = new {1}(this, {2});", EntityVariableName, ThisPD.DataTypeEntityDef.ClassNames.ClassName(), parentVariable);
                    body.Add("    {0}.Initialize{1}({2});", parentVariable, ThisPD.Names.BackingProperty(), EntityVariableName);
                    if ((ThisPD.ForeignKeyPropertyDef.IsSomething()))
                    {
                        body.Add("    {0}.Initialize{1}({2});", EntityVariableName, ThisPD.ForeignKeyPropertyDef.Names.BackingProperty(), parentVariable);
                    }

                    this.AddDataElements(body, ThisDE, EntityVariableName, DtoVariableName);
                    body.Add("}");
                    body.Add("else");
                    body.Add("{");
                    body.Add("    {0}.Initialize{1}();", parentVariable, ThisPD.Names.BackingProperty());
                    body.Add("}");


                }

            }

            /// <summary>
            /// 
            /// </summary>
            private void AddValueTypeDataElements(Body body, DataElement parentDE, string parentVariable, string dtoName)
            {
                var _with5 = body;
                if (parentDE.IncludeAllScalarTypeProperties)
                {
                    _with5.Add("{0}.InitializeAllScalarProperties({1});", parentVariable, dtoName);
                }
                else
                {
                    //Always create the primary key property.
                    var PrimaryKeyPD = parentDE.DataElements[0].PropertyDef.EntityDef.PrimaryKeyPropertyDef;
                    Debug.Assert(PrimaryKeyPD.IsSomething(), "Could not find primary key property definition in procedure AggregateDefFile::AddValueTypeDataElements");
                    _with5.Add("{0}.Initialize{1}({2}.{3});", parentVariable, PrimaryKeyPD.Names.BackingProperty(), dtoName, PrimaryKeyPD.Name);
                    foreach (DataElement ThisDE in parentDE.DataElements.ValueTypes)
                    {
                        if (ThisDE.PropertyDef != PrimaryKeyPD)
                        {
                            _with5.Add("{0}.Initialize{1}({2}.{3});", parentVariable, ThisDE.PropertyDef.Names.BackingProperty(), dtoName, ThisDE.PropertyDef.Name);
                        }
                    }
                }
            }

            /// <summary>
            /// To identify a unique suffix to aid in the creation of unique variable names.
            /// </summary>
            private int GetNextVariableSuffix()
            {
                _suffix += 1;
                return _suffix;
            }
            private static int _suffix = 0;

            #endregion

            #region AddSynchronizeDtosProcedure

            /// <summary>
            /// 
            /// </summary>
            private void AddSynchronizeDtosProcedure(AggregateDef thisAD)
            {
                Region ThisRegion = this.AddRegion("SynchronizeDtos");

                var CustomSynchronizeDtosProcedure = ThisRegion.AddProcedure("CustomSynchronizeDtos", Modifier.Partial);
                CustomSynchronizeDtosProcedure.Documentation.Summary = "Provides a way for the developer to custom synchronize the dtos with the object graph for this aggregate.  Be sure to set the _SynchronizeDtosIsHandled field to true to avoid having the auto-generated synchronization code executed.";

                var SynchronizeDtosIsHandledField = ThisRegion.AddField("_SynchronizeDtosIsHandled", Modifier.Private, "bool");
                SynchronizeDtosIsHandledField.Documentation.Summary = "Informs the auto-generated SynchronizeDtos method that synchronization has already been handled and it does not need to do anything.";
                SynchronizeDtosIsHandledField.DefaultValue = "false";


                var ThisProcedure = ThisRegion.AddProcedure("SynchronizeDtos", Modifier.ProtectedOverride);
                ThisProcedure.Documentation.Summary = "";
                var EntityVariableName = "This" + thisAD.EntityDef.Name;

                ThisProcedure.Body.AddComment("Offers the developer a way to synchronize the dtos by hand.");
                ThisProcedure.Body.Add("this.CustomSynchronizeDtos();");
                ThisProcedure.Body.Add("if (this._SynchronizeDtosIsHandled) return;");
                ThisProcedure.Body.AddBlankLine();

                ThisProcedure.Body.Add("try");
                ThisProcedure.Body.Add("{");


                if (thisAD.RootIsCollection)
                {
                    ThisProcedure.Body.Add("foreach (var {0} in this.Root)", EntityVariableName);
                    ThisProcedure.Body.Add("{");
                    ThisProcedure.Body.Add("    {0}.SynchronizeDto(this.DtoHost.Root{1});", EntityVariableName, thisAD.EntityDef.Name.ToPlural());
                    AddSynchronizeDataElements(ThisProcedure.Body, thisAD.DataElement, EntityVariableName);
                    ThisProcedure.Body.Add("}");
                }
                else
                {
                    ThisProcedure.Body.Add("this.Root.SynchronizeDto(this.DtoHost.Root{0});", thisAD.EntityDef.Name.ToPlural());
                    AddSynchronizeDataElements(ThisProcedure.Body, thisAD.DataElement, "this.Root");
                }

                ThisProcedure.Body.Add("}");
                ThisProcedure.Body.Add("    catch (Exception ex)");
                ThisProcedure.Body.Add("{");
                ThisProcedure.Body.Add("    throw new Exception(\"There was an unexpected problem while synchronizing DTOs.  See inner exception for more info.\", ex);");
                ThisProcedure.Body.Add("}");

            }


            private void AddSynchronizeDataElements(Body body, DataElement parentDE, string parentVariable)
            {
                AddSynchronizeEntityDataElements(body, parentDE, parentVariable);
                AddSynchronizeCollectionDataElements(body, parentDE, parentVariable);
            }


            private void AddSynchronizeEntityDataElements(Body body, DataElement parentDE, string parentVariable)
            {
                foreach (DataElement ThisDE in parentDE.DataElements.EntityTypes)
                {
                    string EntityVariableName = ThisDE.PropertyDef.Dt.ToBaseName() + this.GetNextVariableSuffix();
                    var _with3 = body;
                    _with3.Add("var {0} = {1}.{2};", EntityVariableName, parentVariable, ThisDE.PropertyDef.Name);
                    _with3.Add("if ({0} != null)", EntityVariableName);
                    _with3.Add("{");
                    _with3.Add("    {0}.SynchronizeDto(this.DtoHost.{1});", EntityVariableName, ThisDE.PropertyDef.DataTypeEntityDef.Name.ToPlural());
                    this.AddSynchronizeDataElements(body, ThisDE, EntityVariableName);
                    _with3.Add("}");
                }


            }

            private void AddSynchronizeCollectionDataElements(Body body, DataElement parentDE, string parentVariable)
            {
                var _with4 = body;
                foreach (DataElement ThisDE in parentDE.DataElements.CollectionTypes)
                {
                    string EntityVariableName = ThisDE.PropertyDef.Dt.ToBaseName() + this.GetNextVariableSuffix();
                    _with4.Add("foreach (var {0} in {1}.{2})", EntityVariableName, parentVariable, ThisDE.PropertyDef.Name);
                    _with4.Add("{");
                    _with4.Add("    {0}.SynchronizeDto(this.DtoHost.{1});", EntityVariableName, ThisDE.PropertyDef.DataTypeEntityDef.Name.ToPlural());
                    this.AddSynchronizeDataElements(body, ThisDE, EntityVariableName);
                    _with4.Add("}");
                }
            }

            #endregion

            #region AddUserHasPermissionToSaveProcedure

            private void AddUserHasPermissionToSaveProcedure(AggregateDef thisAD)
            {
                var ThisProcedure = this.AddRegion("UserHasPermissionToSave").AddProcedure("UserHasPermissionToSave", Modifier.ProtectedOverride, "bool");
                ThisProcedure.Documentation.Summary = "Identifies if the current user has permission to save all the updateable entities in this AggregateDef.";
                var UpdateableEntities = thisAD.GetUpdateableEntities();

                if (UpdateableEntities.Count > 0)
                {
                    if (thisAD.ClassName == "UserLogInAggregateDef")
                    {
                        ThisProcedure.Body.Add("return true;");

                    }
                    else
                    {
                        ThisProcedure.Body.Add("return ");
                        for (int index = 1; index <= UpdateableEntities.Count; index++)
                        {
                            if (UpdateableEntities.Count == 1 || index == UpdateableEntities.Count)
                            {
                                ThisProcedure.Body.Add("    this.User.CanUpdate{0}();", UpdateableEntities[index - 1].Name);
                            }
                            else
                            {
                                ThisProcedure.Body.Add("    this.User.CanUpdate{0}() &&", UpdateableEntities[index - 1].Name);
                            }
                        }
                    }

                }
                else
                {
                    ThisProcedure.Body.Add("return false;");
                }
            }


            #endregion

            #region AddLoadAsyncForUserLogInAggregate

            private void AddLoadAsyncForUserLogInAggregate()
            {
                Region ThisRegion = this.AddRegion("LoadAsync");
                var ThisProcedure = ThisRegion.AddProcedure("LoadAsync", Modifier.Public, "Task");
                ThisProcedure.Documentation.Summary = "A special LoadAsync method for logging in a user.";
                ThisProcedure.Parameters.Add("userName", "string");
                ThisProcedure.Parameters.Add("password", "string");
                ThisProcedure.Body.Add("return Task.Factory.StartNew(() =>");
                ThisProcedure.Body.Add("{");
                ThisProcedure.Body.Add("    string Filter = string.Format(\"SELECT UserID FROM [User] WHERE UserName = '{0}' AND Password = '{1}'\", userName, password);");
                ThisProcedure.Body.Add("    var ThisServiceProxy = DomainDataServiceClient.Create();");
                ThisProcedure.Body.Add("    this.DtoHost = ThisServiceProxy.GetDtoHost(this.GetType().FullName, Filter, userName, password);");
                ThisProcedure.Body.Add("    this.BuildObjectGraph();");
                ThisProcedure.Body.Add("});");
            }


            #endregion
        }

        #endregion

    }
}
